# coding:utf-8
import time
from abc import ABCMeta
from pexpect import run

import app
from app.modules.apps.container import ContainerApp
from app.modules.apps.container_ad import AdempiereApp
from app.modules.apps.container_httpd import HttpdPhpApp
from app.modules.apps.container_springboot import SpringBootApp
from app.modules.apps.container_tomcat import TomcatScApp, TomcatDepApp, \
    TomcatApp, UscheduleTomcatApp, TomcatOpenApiApp
from app.modules.common import pipeline_manager
from app.modules.common.network import call_agent_api
from app.modules.common.pipelines import Pipeline, PipelineEvent, \
    PipelineStatus, os
from app.modules.common.port import protected_port_group_pool
from app.modules.common.tasks import *
from app.modules.database import DatabasePipelinesFactory
from config import BeetleConfig

logger = logging.getLogger(__name__)


def do_init_container_app(app_id, zone_code, app_type, zone_alias, port,
                          db_name, mem_level, timezone):
    if app_id == '' or app_type == '':
        raise ValueError('app_id and app_type can\'t be null')
    try:
        (zone_title_code, app_name) = app_id.split(':')
    except:
        traceback.print_exc()
        raise ValueError(
            'app_id must could be split to zone_code and app_name by ":", but the actually is "%s"' % app_id)

    _zone_home_path = '/usr/local/app/%s' % zone_code
    nas_app_package_path = '/mnt/winitRelease/latest.product/%s' % app_name

    if app_type == 'adempiere':
        return AdempiereApp(app_id = app_id,
                            app_name = app_name,
                            zone_home_path = _zone_home_path,
                            nas_app_package_path = None,
                            zone_alias = zone_alias,
                            port = port,
                            db_name = db_name)
    elif app_type == 'tomcat':

        if app_name == 'sc':
            return TomcatScApp(app_id = app_id,
                               zone_home_path = _zone_home_path,
                               nas_app_package_path = nas_app_package_path,
                               zone_alias = zone_alias,
                               port = port,
                               mem_level = mem_level,
                               timezone = timezone)
        elif app_name == 'dep':
            return TomcatDepApp(app_id = app_id,
                                app_name = app_name,
                                zone_home_path = _zone_home_path,
                                nas_app_package_path = nas_app_package_path,
                                zone_alias = zone_alias,
                                port = port,
                                db_name = db_name,
                                mem_level = mem_level,
                                timezone = timezone)
        elif app_name == 'openapi':
            return TomcatOpenApiApp(app_id = app_id,
                                    app_name = app_name,
                                    zone_home_path = _zone_home_path,
                                    nas_app_package_path = nas_app_package_path,
                                    zone_alias = zone_alias,
                                    port = port,
                                    mem_level = mem_level,
                                    timezone = timezone)

        elif app_name == 'dubbo-admin':
            return TomcatApp(app_id = app_id,
                             app_name = app_name,
                             zone_home_path = _zone_home_path,
                             zone_alias = zone_alias,
                             port = port,
                             nas_app_package_path = nas_app_package_path,
                             war_config_path = '/WEB-INF',
                             war_exposed_name = 'ROOT',
                             mem_level = mem_level,
                             timezone = timezone)
        elif app_name == 'uschedule':

            return UscheduleTomcatApp(app_id = app_id,
                                      app_name = app_name,
                                      zone_home_path = _zone_home_path,
                                      nas_app_package_path = nas_app_package_path,
                                      zone_alias = zone_alias,
                                      port = port,
                                      mem_level = mem_level,
                                      timezone = timezone)
        else:

            return TomcatApp(app_id = app_id,
                             app_name = app_name,
                             zone_home_path = _zone_home_path,
                             nas_app_package_path = nas_app_package_path,
                             zone_alias = zone_alias,
                             port = port,
                             mem_level = mem_level,
                             timezone = timezone)
    elif app_type == 'php':

        if app_name == 'booking':
            return HttpdPhpApp(app_id = app_id,
                               app_name = app_name,
                               zone_home_path = _zone_home_path,
                               nas_app_package_path = nas_app_package_path,
                               zone_alias = zone_alias,
                               sub_doc_dir = '/Agent',
                               port = port)
        elif app_name == 'developer':
            return HttpdPhpApp(app_id = app_id,
                               app_name = app_name,
                               zone_home_path = _zone_home_path,
                               nas_app_package_path = nas_app_package_path,
                               zone_alias = zone_alias,
                               sub_doc_dir = '/public',
                               port = port)
        else:
            return HttpdPhpApp(app_id = app_id,
                               app_name = app_name,
                               zone_home_path = _zone_home_path,
                               nas_app_package_path = nas_app_package_path,
                               zone_alias = zone_alias,
                               port = port)

    elif app_type == 'spring-boot':
        return SpringBootApp(app_id = app_id,
                             app_name = app_name,
                             zone_home_path = _zone_home_path,
                             zone_alias = zone_alias,
                             port = port,
                             nas_app_package_path = nas_app_package_path,
                             mem_level = mem_level,
                             timezone = timezone)

    else:
        raise ValueError('Unavlilable app_type:%s' % app_type)


def init_app_piplelines(apps):
    protected_port_group_pool.clean()

    for app in apps:

        if (app.zone is None):
            print "App's zone is null, app_id: %s" % app.app_id

        try:
            pips = init_container_app_pipeline(app_id = app.app_id,
                                               zone_code = app.zone_code,
                                               app_type = app.app_type,
                                               zone_alias = app.zone.zone_alias,
                                               port = app.port,
                                               db_name = app.db_name,
                                               db_type = app.db_type,
                                               mem_level = app.mem_level,
                                               timezone = app.zone.timezone)
            for pip in pips:
                pip.set_app(app)
                pipeline_manager.register_pipeline(pip)
                log.info('Initializing pip:' + pip.name)

        except:
            # just give a warn, not to stop.
            logger.warn(
                'A error is occur with app pipeline initializing. %s' % app.app_id)
            logger.warn(traceback.format_exc())
            print "App init error, app_id: %s" % app.app_id
            traceback.print_exc()


# create a group of single instance of container_pipeline.
def init_container_app_pipeline(app_id, zone_code, app_type, zone_alias, port,
                                db_name, db_type, mem_level, timezone):
    if app_id not in app.container_pool:
        container = do_init_container_app(app_id = app_id, zone_code = zone_code,
                                          app_type = app_type,
                                          zone_alias = zone_alias, port = port,
                                          db_name = db_name, mem_level = mem_level,
                                          timezone = timezone)

        # 初始化容器的同时,将需要保护的端口注册到port_group_pool中
        protected_port_group_pool.register_port_group(group_key = app_id,
                                                      ports = container.get_ports())
        if protected_port_group_pool.is_port_free(group_key = app_id):
            logger.info(u'====>应用%s端口未被占用, 准备执行端口保护' % app_id)
            protected_port_group_pool.protect_port_group(group_key = app_id)
        elif not container.is_started(accurately = True):
            logger.info(
                u'====>应用%s端口已经被占用,但应用未启动, 准备干掉干扰的进程, 强制执行端口保护' % app_id)
            protected_port_group_pool.protect_port_group(group_key = app_id,
                                                         force = True)

        else:
            logger.info(
                u'====>应用%s端口已经被占用,但应用未启动, 准备干掉干扰的进程, 强制执行端口保护' % app_id)

        logger.info(u'应用%s初始化完毕' % app_id)
        # 如果应用没有启动, 就保护(提前占用)这些端口,如果应用已经启动, 就跳过.
        # if not container.is_started(accurately=True):
        # protected_port_group_pool.protect_port_group(group_key=app_id)

        app.container_pool[app_id] = container

    _tasks = []

    app_id = str(app_id)

    _group_id = DatabasePipelinesFactory.build_pipeline_group(db_name = db_name,
                                                              db_type = db_type)

    _restart_pipeline = ContainerAppPipeline(app_id = app_id,
                                             name = '%s_restart' % app_id,
                                             group = _group_id)
    _restart_pipeline.add_task(
        RestartAppTask('%s_restart_task' % app_id, app_id))

    _tasks.append(_restart_pipeline)

    # php need not restart app.
    if app_type != 'php':
        _stop_pipeline = ContainerAppPipeline(app_id = app_id,
                                              name = '%s_stop' % app_id,
                                              group = _group_id)
        _stop_pipeline.add_task(StopAppTask('%s_stop_task' % app_id, app_id))
        _tasks.append(_stop_pipeline)

    if app_type == 'adempiere':  # adempiere应用的安装超时设置为7200秒
        _install_pipeline = ContainerAppPipeline(app_id = app_id,
                                                 name = '%s_install' % app_id,
                                                 group = _group_id,
                                                 timeout = 7200)
    else:
        _install_pipeline = ContainerAppPipeline(app_id = app_id,
                                                 name = '%s_install' % app_id,
                                                 group = _group_id)

    _install_pipeline.add_task(
        StopAppTask(name = '%s_install_stop_task' % app_id, app_id = app_id))

    # php类型的应用,无需删除,只需要覆盖即可.
    if app_type != 'php':
        _install_pipeline.add_task(
            UnInstallAppTask(name = '%s_install_uninstall_task' % app_id,
                             app_id = app_id))

    _install_pipeline.add_task(
        InstallAppTask(name = '%s_install_install_task' % app_id, app_id = app_id))

    # 因为大部分应用必须相互都安装之后, 才能启动, 所以这次去掉了安装过程中的重启任务, 这样可以避免浪费启动应用造成的cpu消耗----by 王檬 2017-01-02
    # php是个例外, 它必须要重启,否则配置文件不会生效.
    if app_type == 'php':
        _install_pipeline.add_task(
            RestartAppTask(name = '%s_install_restart_task' % app_id,
                           app_id = app_id))

    _tasks.append(_install_pipeline)

    return _tasks


class ContainerAppPipeline(Pipeline):
    def __init__(self, name, app_id, group, timeout = 600):
        super(ContainerAppPipeline, self).__init__(name = name, group = group,
                                                   timeout = timeout)

        self._app_id = app_id
        self._app_type = group

    def _before_execute(self):
        # protected_port_group_pool.clean()
        pass

    def to_dict(self):
        if self.err_msg is not None and self.err_msg != '':
            print self.err_msg

        _dict = {
            'name': self.name,
            'group': self.group,
            'status': self.status,
            'pid': self.pid,
            'progress': self.progress,
            'lasted_started': self.lasted_started.strftime(
                '%Y-%m-%d %H:%M:%S') if self.lasted_started else None,
            'lasted_finished': self.lasted_finished.strftime(
                '%Y-%m-%d %H:%M:%S') if self.lasted_finished else None,
            'err_msg': self.err_msg,
        }

        return _dict


class ContainerAppTask(Task):
    __metaclass__ = ABCMeta

    def __init__(self, name, app_id):
        Task.__init__(self, name)
        self._app_id = app_id


class RestartAppTask(ContainerAppTask):
    def __do_execute__(self, params = {}):

        try:

            call_agent_api('/apps/lock_free_port', params = {
                'switch': 'free',
                'app_id': self._app_id
            })

            time.sleep(1)

            container_app = app.container_pool.get(self._app_id)
            container_app.restart()
        except BaseException, ex:

            err_msg = 'Failed with restart app. %s ' % ex.message

            # 目前发生异常一样会启动服务, 所以不用lock.
            # call_agent_api('/apps/lock_free_port', params={
            #     'switch': 'lock',
            #     'app_id': self._app_id
            # })

            raise TaskExcecuteError(err_msg)


class InstallAppTask(ContainerAppTask):
    def __do_execute__(self, params = {}):
        try:
            container_app = app.container_pool.get(self._app_id)
            container_app.install_app()
        except BaseException, ex:

            logger.error(traceback.format_exc())

            err_msg = 'Failed with install app. %s' % ex.message

            call_agent_api('/apps/lock_free_port', params = {
                'switch': 'lock',
                'app_id': self._app_id
            })

            raise TaskExcecuteError(err_msg)


class UnInstallAppTask(ContainerAppTask):
    def __do_execute__(self, params = {}):
        try:

            container_app = app.container_pool.get(self._app_id)
            container_app.uninstall_app()
        except BaseException, ex:
            err_msg = 'Failed with uninstall app. %s' % ex.message

            call_agent_api('/apps/lock_free_port', params = {
                'switch': 'lock',
                'app_id': self._app_id
            })

            raise TaskExcecuteError(err_msg)


class StopAppTask(ContainerAppTask):
    def __do_execute__(self, params = {}):
        try:
            _params = params.get('package_path')

            container_app = app.container_pool.get(self._app_id)

            container_app.stop()

            time.sleep(1)

        except BaseException, ex:
            err_msg = 'Failed with stop app. %s' % ex.message

            raise TaskExcecuteError(err_msg)

        finally:

            call_agent_api('/apps/lock_free_port', params = {
                'switch': 'lock',
                'app_id': self._app_id
            })


# ContainerApp的处理事件, 用于前端展示应用发布列表时, 及时收到app操作的状态通知.
class ContainerAppEvent(PipelineEvent):
    def __init__(self, app, pipeline):
        self._app = app
        self._pipeline = pipeline

    def to_dict(self):
        _dict = {
            'name': 'container_app_event',
            'event': 'container_app_event',
            'pipeline': self._pipeline.to_dict(),
            'app': self._app.to_dict(),
        }

        return _dict

    def __str__(self):
        return (
            "<{class_name}("
            "app=\"{self._app}\","
            "pipeline=\"{self._pipeline}\""
            ")>".format(
                class_name = self.__class__.__name__,
                self = self
            )
        )


def app_log(content = '', is_error = False):
    if not os.path.exists(BeetleConfig.INIT_APP_LOG_PATH):
        run('/bin/bash -c "mkdir -p %s"' % BeetleConfig.INIT_APP_LOG_PATH)

    logfile = BeetleConfig.INIT_APP_LOG_PATH + '/init.log'
    if is_error:
        log_info = '[ERROR] %s [at %s] \n' % (
            content,
            time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))
    else:
        log_info = '[INFO] %s [at %s] \n' % (
            content,
            time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))

    with open(logfile, 'a') as _f:
        _f.write(log_info)


def clear_log():
    if not os.path.exists(BeetleConfig.INIT_APP_LOG_PATH):
        run('/bin/bash -c "mkdir -p %s"' % BeetleConfig.INIT_APP_LOG_PATH)
    logfile = BeetleConfig.INIT_APP_LOG_PATH + '/init.log'
    with open(logfile, 'w') as _f:
        _f.write('')


class AppInitializer:
    STATUS_INITIALIZING = 'initializing'

    STATUS_READY = 'ready'

    def __init__(self):
        self.status = AppInitializer.STATUS_READY

    def do_init(self, apps = [], app_pkg_version = {}):
        """
        根据应用记录列表和app版本标记列表, 安装当前服务器的所有应用.
        :param apps:
        :param app_pkg_version:
        :return:
        """

        try:

            self.status = AppInitializer.STATUS_INITIALIZING

            clear_log()

            app_log(
                content = '-----------------------------Start to install app.------------------------------')

            app_log(content = str(app_pkg_version))

        except:
            print traceback.format_exc()
            logger.error(traceback.format_exc())
            return
        finally:
            self.status = AppInitializer.STATUS_READY

        for _app in apps:

            try:

                _app_id = _app.app_id
                _app_name = _app.app_name
                _app_pkg_version = app_pkg_version.get(_app_name)
                _pipeline_name = _app_id + "_install"

                pipeline = pipeline_manager.get_pipeline(_pipeline_name)
                if not isinstance(pipeline, ContainerAppPipeline):
                    raise BaseException(
                        'Only container app pipeline can be sepceific.')

                container_app = app.container_pool.get(_app_id)

                if not isinstance(container_app, ContainerApp):
                    raise BaseException(
                        'Only container app pipeline can be sepceific.')

                if not _app.on_beetle == 'true':

                    # 无需beetle提供版本信息的服务, 可以直接启动安装进程.

                    msg = 'Install %s of default version.' % _app_id
                    app_log(content = msg, is_error = False)

                    container_app.set_runtime_param('', '', None)
                    pipeline_manager.start_pipeline(
                        pipeline_name = _pipeline_name, pipeline_params = {})

                    pass

                elif _app_pkg_version is not None:

                    # 1.从版本字典中, 获取应用的版本信息
                    _path = _app_pkg_version.get('package_path')
                    _version = _app_pkg_version.get('version')
                    _version_type = _app_pkg_version.get('version_type')

                    if os.path.isfile(_path):
                        _path = os.path.dirname(_path)

                    msg = 'Install %s _path=%s _version=%s _version_type=%s' % (
                        _app_id, _path, _version, _version_type)

                    app_log(content = msg, is_error = False)

                    container_app.set_runtime_param(_path, _version, None)

                    pipeline_manager.start_pipeline(
                        pipeline_name = _pipeline_name, pipeline_params = {})

                else:
                    msg = 'None version app %s has been found' % _app_id
                    print msg
                    app_log(content = msg)
            except:
                # 单个应用出现安装异常, 不能退出, 要继续尝试安装其他应用.
                print traceback.format_exc()
                logger.error(traceback.format_exc())
                msg = 'Install %s faild.' % _app.app_id
                app_log(content = msg)
                app_log(content = traceback.format_exc())

                continue

        app_log(
            content = '##################################Finish#########################################')

        self.status = AppInitializer.STATUS_READY


def set_app_runtime_info(app_modle, check_running_accurately = False):
    # 1.获取当前app正在绑定运行的pipe
    app_pipelines = (app_modle.app_id + '_install', app_modle.app_id + '_stop',
                     app_modle.app_id + '_restart')

    running_pipe = None

    for pipeline_name in app_pipelines:
        pipe = pipeline_manager.get_pipeline(pipeline_name)
        if pipe is None:
            # 部分app没有stop这类的pip, 如果找不到可以跳出.
            continue
        if pipe.status == PipelineStatus.PENDING or pipe.status == PipelineStatus.RUNNING:
            running_pipe = pipeline_name
            break

    app_modle.running_pipeline = running_pipe

    # 2.根据appid找到pipe后,顺着Container,调查该应用是否处于运行状态.
    container_app = app.container_pool.get(app_modle.app_id)
    if container_app:
        app_modle.is_running = container_app.is_started(
            accurately = check_running_accurately)
    else:
        log.error(u'%s对应的app不存在 ' % app_modle.app_id)
    # log.warn('当前应用端口号:%s, 是否启动:%s' % (app_modle.port, app_modle.is_running))

    # print str(app_modle.port) + ':' + str(container_app.is_started()) + ":" + str(app_modle.is_running)

    return app_modle


app_initializer = AppInitializer()
